﻿using System;
using System.Collections.Generic;
using System.IO;

namespace ooWii.IO
{
	public class SparseStream : Stream
	{
		#region SparseStream

		long length = 0;
		long position = 0;
		Stream stream;
		IList<Region> regions = new List<Region>();

		public SparseStream( Stream stream )
		{
			this.stream = stream;
		}

		public void Map( long logical, long physical, long length )
		{
			UnMap( logical, length );

			int i = regions.Count - 1;
			while( true )
			{
				if( i < 0 || logical > regions[ i ].logical + regions[ i ].length )
				{
					Region newRegion = new Region();
					newRegion.logical = logical;
					newRegion.physical = physical;
					newRegion.length = length;
					regions.Insert( ++i, newRegion );
					break;
				}

				Region region = regions[ i ];

				if( logical == region.logical + region.length &&
					physical == region.physical + region.length )
				{
					region.length += length;
					break;
				}

				i--;
			}

			if( i < regions.Count - 1 && regions[ i + 1 ].logical == logical + length && regions[ i + 1 ].physical == physical + length )
			{
				regions[ i ].length += regions[ i + 1 ].length;
				regions.RemoveAt( i + 1 );
			}
		}

		public void UnMap( long logical, long length )
		{
			int i = regions.Count - 1;
			while( i >= 0 )
			{
				Region region = regions[ i ];

				if( logical >= region.logical + region.length )
				{
					break;
				}

				long leaveFromStart = logical - region.logical;
				if( leaveFromStart > 0 )
				{
					if( region.length > leaveFromStart )
					{
						long leaveAtEnd = ( region.logical + region.length ) - ( logical + length );
						if( leaveAtEnd > 0 )
						{
							Region splitRegion = new Region();
							splitRegion.logical = logical + length;
							splitRegion.physical = region.physical + ( splitRegion.logical - region.logical );
							splitRegion.length = leaveAtEnd;
							regions.Insert( i + 1, splitRegion );
						}

						region.length = leaveFromStart;
					}
				}
				else
				{
					long removeFromStart = ( logical + length ) - region.logical;
					if( removeFromStart > 0 )
					{
						if( region.length > removeFromStart )
						{
							region.logical += removeFromStart;
							region.physical += removeFromStart;
							region.length -= removeFromStart;
						}
						else
						{
							regions.RemoveAt( i );
						}
					}
				}

				i--;
			}
		}

		#endregion

		#region Stream

		public override bool CanRead
		{
			get
			{
				return true;
			}
		}

		public override bool CanSeek
		{
			get
			{
				return true;
			}
		}

		public override bool CanWrite
		{
			get
			{
				return false;
			}
		}

		public override void Flush()
		{
		}

		public override long Length
		{
			get
			{
				return length;
			}
		}

		public override long Position
		{
			get
			{
				return position;
			}
			set
			{
				if( value < 0 )
				{
					throw new ArgumentOutOfRangeException( "value", "Non-negative number required." );
				}

				position = value;
			}
		}

		public override int Read( byte[] buffer, int offset, int count )
		{
			if( count > length - position )
			{
				count = (int)( length - position );
			}

			long newPosition = position;

			int read = 0;

			while( count != 0 )
			{
				int i = regions.Count - 1;
				while( i > 0 )
				{
					if( newPosition >= regions[ i ].logical )
					{
						break;
					}

					i--;
				}

				int bytes = count;

				long offsetInRegion = newPosition - regions[ i ].logical;

				if( regions[ i ].length > offsetInRegion )
				{
					long left = regions[ i ].length - offsetInRegion;

					if( bytes > left )
					{
						bytes = (int)left;
					}

					stream.Position = regions[ i ].physical + offsetInRegion;
					if( stream.Read( buffer, offset, bytes ) != bytes )
					{
						throw new WiiException( "Cannot read" );
					}
				}
				else
				{
					long left;

					if( i < regions.Count - 1 )
					{
						left = regions[ i + 1 ].logical - newPosition;
					}
					else
					{
						left = length - newPosition;
					}

					if( bytes > left )
					{
						bytes = (int)left;
					}

					Array.Clear( buffer, offset, bytes );
				}

				offset += bytes;
				read += bytes;
				count -= bytes;

				newPosition += bytes;
			}

			position = newPosition;

			return read;
		}

		public override long Seek( long offset, SeekOrigin origin )
		{
			long newPosition;

			switch( origin )
			{
			case SeekOrigin.Begin:
				newPosition = offset;
				break;

			case SeekOrigin.Current:
				newPosition = this.Position + offset;
				break;

			case SeekOrigin.End:
				newPosition = this.Length + offset;
				break;

			default:
				throw new ArgumentException( "origin" );
			}

			if( newPosition < 0 )
			{
				throw new IOException( "An attempt was made to move the file pointer before the beginning of the file." );
			}

			position = newPosition;
			return position;
		}

		public override void SetLength( long value )
		{
			if( length < 0 )
			{
				throw new ArgumentOutOfRangeException( "value", "Non-negative number required." );
			}

			length = value;
		}

		public override void Write( byte[] buffer, int offset, int count )
		{
			throw new NotSupportedException();
		}

		#endregion

		class Region
		{
			#region Region

			public long logical;
			public long physical;
			public long length;

			#endregion
		}
	}
}
